﻿using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Mail;
using System.Text;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
using System.Xml.Linq;
using FreeImageAPI;
using Mindfor.Web.Config;
using Mindfor.Web.Data;
using NHibernate;
using NHibernate.Linq;
using Mindfor.Web.Models;
using System.ComponentModel;
using System.Linq.Expressions;

namespace Mindfor.Web
{
	/// <summary>
	/// Base CMS controller which implements cms options loading, such as configuration and data provider.
	/// </summary>
	public abstract class CmsController : Controller
	{
		ISession m_data;
		Uri m_siteUri;
		SmtpClient m_smtp;
		
		/// <summary>
		/// Gets data session.
		/// </summary>
		public ISession Data
		{
			get
			{
				if (m_data == null)
				{
					m_data = CmsApplication.DataProvider.GetCurrentSession();
					if (m_data == null)
						throw new InvalidOperationException("Can not get current data session.");
				}
				return m_data;
			}
		}

		/// <summary>
		/// Gets whether running in debug mode.
		/// </summary>
		public bool IsDebug
		{
			get
			{
#if DEBUG
				return true;
#else
				return false;
#endif
			}
		}

		/// <summary>
		/// Gets site url that is determined from current request.
		/// </summary>
		public Uri SiteUrl
		{
			get
			{
				if (m_siteUri == null)
				{
					Uri requestUrl = this.Request.Url;
					string siteUrl = requestUrl.AbsoluteUri.Substring(0,
						requestUrl.AbsoluteUri.Length - requestUrl.PathAndQuery.Length);
					m_siteUri = new Uri(siteUrl, UriKind.Absolute);
				}
				return m_siteUri;
			}
		}

		/// <summary>
		/// Gets controller module.
		/// </summary>
		public ModuleBase Module { get; private set; }

		/// <summary>
		/// Gets current authenticated user. Returns null if user is anonymous.
		/// </summary>
		public new User User { get; private set; }

		/// <summary>
		/// Gets current request language.
		/// </summary>
		public Language Language { get; private set; }

		/// <summary>
		/// Gets whether user is authenticated.
		/// </summary>
		public bool IsAuthenticated
		{
			get { return this.User != null; }
		}

		#region For page module

		/// <summary>
		/// Gets current request page.
		/// </summary>
		public Page CurrentPage
		{
			get { return ViewData["Global.CurrentPage"] as Page; }
		}

		/// <summary>
		/// Gets current page text.
		/// </summary>
		public PageText CurrentPageText
		{
			get { return ViewData["Global.CurrentPageText"] as PageText; }
		}

		/// <summary>
		/// Gets main action name if current action is sub-action.
		/// If current action is main action then returns null.
		/// </summary>
		public string MainActionName
		{
			get { return ViewData["Global.MainActionName"] as string; }
		}

		/// <summary>
		/// Gets whether current action is main- or sub-.
		/// If false, then CurrentPage and CurrentPageText is taken from main action.
		/// </summary>
		public bool IsMainAction
		{
			get { return MainActionName == null; }
		}

		/// <summary>
		/// Gets page title.
		/// </summary>
		public PageTitleCollection PageTitle { get; private set; }

		#endregion

		/// <summary>
		/// Initializes new instance.
		/// </summary>
		public CmsController()
		{
		}

		/// <summary>
		/// Initializes class before invoke action.
		/// </summary>
		/// <param name="requestContext">Request context.</param>
		protected override void Initialize(RequestContext requestContext)
		{
			// base initialize
			base.Initialize(requestContext);

			// get module
			string moduleName = requestContext.RouteData.Values["module"] as string;
			if (moduleName != null)
			{
				Module = ModuleFactory.Default.Modules[moduleName];
				HttpContext.Items["module"] = Module;
			}

			// get current user
			if (base.User != null && base.User.Identity != null &&
				base.User.Identity.IsAuthenticated)
				User = Data.Query<User>().GetUser(base.User.Identity.Name);
			
			// get current language and culture
			string langUrlName = (string)requestContext.RouteData.Values["lang"];
			if (String.IsNullOrEmpty(langUrlName))
				langUrlName = CmsConfig.Default.DefaultLanguage;

			if (!String.IsNullOrEmpty(langUrlName))
				Language = Data.Query<Language>().FirstOrDefault(r => r.UrlName == langUrlName);
			if (Language == null)
				Language = Data.Query<Language>().FirstOrDefault();				
			
			if (Language != null)
			{
				CultureInfo culture = null;
				try
				{
					culture = CultureInfo.GetCultureInfo(Language.Culture);
				}
				catch (Exception ex)
				{
					throw new InvalidOperationException("Cannot find \"" + Language.Culture +  "\" culture.", ex);
				}

				System.Threading.Thread.CurrentThread.CurrentCulture = culture;
				System.Threading.Thread.CurrentThread.CurrentUICulture = culture;
			}
			
			ControllerContext.HttpContext.Trace.Write("CmsController", "Initialize end");
		}

		/// <summary>
		/// Adds error to local log and mindfor server log.
		/// </summary>
		/// <param name="ex">Error to add to logs.</param>
		protected void LogError(Exception ex)
		{
			// log local
			try
			{
				Log log = new Log(ex);
				using (ITransaction t = Data.BeginTransaction())
				{
					Data.Save(log);
					t.Commit();
				}
			}
			catch { }

			// log to server
			if (CmsConfig.Default.ApplicationId.HasValue)
				Mindfor.Error.ErrorProxy.SubmitSafe(CmsConfig.Default.ApplicationId.Value, ex);
		}

		/// <summary>
		/// Sets model complex property to value that was loaded from DataProvider by primary key
		/// specified in value provider.
		/// </summary>
		/// <param name="model">Model to update.</param>
		/// <param name="propertyName">Complex property that references another type.</param>
		/// <param name="primaryKeyName">Primary key name in complex property.</param>
		/// <exception cref="TypeLoadException">Occures when complex property is not found.</exception>
		protected void UpdateModelReference(object model, string propertyName, string primaryKeyName)
		{
			if (String.IsNullOrEmpty(propertyName))
				throw new ArgumentNullException(propertyName);
			if (String.IsNullOrEmpty(primaryKeyName))
				throw new ArgumentNullException(primaryKeyName);

			// find properties
			PropertyDescriptor propDescriptor = TypeDescriptor.GetProperties(model)[propertyName];
			if (propDescriptor == null)
				throw new TypeLoadException(String.Format("Can not find property \"{0}\".", propertyName));

			PropertyDescriptor primaryPropDescriptor = TypeDescriptor.GetProperties(propDescriptor.PropertyType)[primaryKeyName];
			if (primaryPropDescriptor == null)
				throw new TypeLoadException(String.Format("Can not find property \"{0}\".", primaryKeyName));

			// get complex property value
			string key = propertyName + "." + primaryKeyName;
			ValueProviderResult valueProviderResult = ValueProvider.GetValue(key);
			if (valueProviderResult != null)
			{
				object id = valueProviderResult.ConvertTo(primaryPropDescriptor.PropertyType);
				if (id != null)
				{
					object reference = Data.Get(propDescriptor.PropertyType, id);
					propDescriptor.SetValue(model, reference);
				}
			}
		}

		/// <summary>
		/// Sets model complex property to value that was loaded from DataProvider by primary key
		/// specified in value provider.
		/// </summary>
		/// <param name="model">Model to update.</param>
		/// <param name="propertyExpression">Lambda expression to complex property that references another type.</param>
		/// <param name="primaryKeyExpression">Lambda expression to primary key in complex property.</param>
		/// <exception cref="TypeLoadException">Occures when complex property is not found.</exception>
		protected void UpdateModelReference<TModel, TProperty, TKey>(TModel model, Expression<Func<TModel, TProperty>> propertyExpression, Expression<Func<TProperty, TKey>> primaryKeyExpression)
		{
			string propertyName = ExpressionHelper.GetExpressionText(propertyExpression);
			string primaryKeyName = ExpressionHelper.GetExpressionText(primaryKeyExpression);
			UpdateModelReference(model, propertyName, primaryKeyName);
		}

		/// <summary>
		/// Sets model complex property to value that was loaded from DataProvider by primary key "Id".
		/// </summary>
		/// <param name="model">Model to update.</param>
		/// <param name="propertyExpression">Lambda expression to complex property that references another type.</param>
		/// <exception cref="TypeLoadException">Occures when complex property is not found.</exception>
		protected void UpdateModelReference<TModel, TProperty>(TModel model, Expression<Func<TModel, TProperty>> propertyExpression)
		{
			string propertyName = ExpressionHelper.GetExpressionText(propertyExpression);
			UpdateModelReference(model, propertyName, "Id");
		}

		#region Custom ActionResults

		/// <summary>
		/// Returns <see cref="FileContentResult"/> which sends image to response.
		/// </summary>
		/// <param name="image">Image to send to response.</param>
		protected virtual FileResult Image(Image image)
		{
			return Image(image, null, -1, -1);
		}

		/// <summary>
		/// Returns <see cref="FileStreamResult"/> with xml document data.
		/// </summary>
		/// <param name="xmlDocument">Xml document to render.</param>
		protected virtual XResult Xml(XDocument xmlDocument)
		{
			return new XResult(xmlDocument);
		}

		/// <summary>
		/// Returns <see cref="FileStreamResult"/> with xml document data.
		/// </summary>
		/// <param name="rootElement">Xml root element.</param>
		protected virtual XResult Xml(XElement rootElement)
		{
			return new XResult(rootElement);
		}

		/// <summary>
		/// Returns <see cref="FileContentResult"/>, which sends image to response.
		/// Image size changes proportional.
		/// </summary>
		/// <param name="image">Image to send to response.</param>
		/// <param name="name">Image file name.</param>
		/// <param name="maxWidth">Max width of image. Use -1 not to analyse width.</param>
		/// <param name="maxHeight">Max height of image. Use -1 not to analyse height.</param>
		protected virtual FileResult Image(Image image, string name, int maxWidth, int maxHeight)
		{
			if (image == null)
				return null;

			const int maxOriginalArea = 100 * 100;
			const int outputImageQuality = 80;

			Image sizedImage;

			// Send the image to the user
			if (image.RawFormat.Guid == ImageFormat.Gif.Guid)
			{
				// Save the gif (but we can't ajust it's size or DPI, sorry)
				sizedImage = image.AdjustGifImage(maxWidth, maxHeight);
			}
			else
			{
				// Ajust and save image
				sizedImage = image.AdjustImage(maxWidth, maxHeight, -1);
			}

			// Set expiration and ETag
			HttpCachePolicyBase cache = HttpContext.Response.Cache;
			TimeSpan expireTs = TimeSpan.FromDays(7);
			cache.SetCacheability(HttpCacheability.Private);
			cache.SetETag(Language.Name + "_" + name + "_" + maxWidth + "_" + maxHeight);
			cache.SetExpires(DateTime.Now.Add(expireTs));
			cache.SetMaxAge(expireTs);


			// Check area
			if (sizedImage.Width * sizedImage.Height > maxOriginalArea)
			{
				Stream sizedImageStream = new MemoryStream();
				sizedImage.Save(sizedImageStream, ImageFormat.Png);
				sizedImageStream.Seek(0, SeekOrigin.Begin);

				FREE_IMAGE_FORMAT fiFormat = FREE_IMAGE_FORMAT.FIF_PNG;

				// Save to progressive jpeg
				FIBITMAP dib = new FIBITMAP();

				if (!dib.IsNull)
					FreeImage.Unload(dib);

				try
				{
					dib = FreeImage.LoadFromStream(sizedImageStream, FREE_IMAGE_LOAD_FLAGS.PNG_IGNOREGAMMA, ref fiFormat);
				}
				catch (BadImageFormatException exx)
				{
					throw new Exception("Сломалось! :( " + fiFormat.ToString(), exx);
				}

				if (!dib.IsNull)
				{
					Stream progressiveStream = new MemoryStream();
					if (FreeImage.SaveToStream(dib, progressiveStream, FREE_IMAGE_FORMAT.FIF_JPEG, FREE_IMAGE_SAVE_FLAGS.JPEG_PROGRESSIVE | FREE_IMAGE_SAVE_FLAGS.JPEG_QUALITYSUPERB))
					{
						// The bitmap was saved to stream but is still allocated in memory, so the handle has to be freed.
						if (!dib.IsNull)
							FreeImage.Unload(dib);

						// Make sure to set the handle to null so that it is clear that the handle is not pointing to a bitmap.
						dib.SetNull();

						progressiveStream.Seek(0, SeekOrigin.Begin);
						return File(progressiveStream, "image/jpeg", name);
					}
				}
			}

			// ELSE
			// Return image as png or jpeg
			if (image.RawFormat.Guid == ImageFormat.Gif.Guid
					|| image.RawFormat.Guid == ImageFormat.Png.Guid)
			{
				byte[] bytes = sizedImage.SaveImage(ImageFormat.Png, outputImageQuality);
				return File(bytes, "image/png", name);
			}
			else
			{
				byte[] bytes = sizedImage.SaveImage(System.Drawing.Imaging.ImageFormat.Jpeg, outputImageQuality);
				return File(bytes, "image/jpeg", name);
			}

		}

		/// <summary>
		/// Returns <see cref="MessageResult"/> which renders view "Message".
		/// ViewData "Message" parameter is set.
		/// </summary>
		/// <param name="message">Message to show.</param>
		/// <param name="links">Additional links to show to user.</param>
		/// <returns>ViewResult which renders view "Message".</returns>
		protected virtual MessageResult Message(string message, params Link[] links)
		{
			return new MessageResult(message, links)
			{
				ViewData = this.ViewData,
				TempData = this.TempData
			};
		}

		/// <summary>
		/// Returns <see cref="MessageResult"/> which renders view "Message".
		/// ViewData "Message" parameter is set.
		/// </summary>
		/// <param name="message">Message to show.</param>
		/// <param name="autoRedirectUrl">Auto redirect url. If <c>Null</c> then auto redirect is disabled.</param>
		/// <param name="links">Additional links to show to user.</param>
		/// <returns>ViewResult which renders view "Message".</returns>
		protected virtual MessageResult Message(string message, string autoRedirectUrl, params Link[] links)
		{
			return new MessageResult(message, links)
			{
				ViewData = this.ViewData,
				TempData = this.TempData,
				AutoRedirectUrl = autoRedirectUrl
			};
		}

		/// <summary>
		/// Returns <see cref="HttpStatusCodeResult"/> with status code 401 if unauthorized and 403 if authorized.
		/// </summary>
		/// <param name="statusDescription">The status description.</param>
		protected virtual HttpStatusCodeResult HttpAccessDenied(string statusDescription = null)
		{
			int code = User == null ? 401 : 403;
			return new HttpStatusCodeResult(code, statusDescription);
		}

		/// <summary>
		/// Returns RedirectResult which redirects to referrer uri.
		/// </summary>
		/// <param name="defaultUrl">Default url to redirect to if referer is null.</param>
		protected RedirectResult RedirectToReferrer(string defaultUrl = "/")
		{
			Uri uri = Request.UrlReferrer;
			return Redirect(uri == null ? defaultUrl : uri.ToString());
		}

		/// <summary>
		/// Returns RedirectResult which redirects to ReturnUrl from request.
		/// If one is null then returns default url redirect.
		/// </summary>
		/// <param name="defaultUrl">Default url to redirect to if referer is null.</param>
		protected virtual RedirectResult RedirectToReturnUrl(string defaultUrl)
		{
			string returnUrl = ViewData["returnUrl"] as string;
			if (String.IsNullOrEmpty(returnUrl))
				returnUrl = Request["returnUrl"];
			if (String.IsNullOrEmpty(returnUrl))
				returnUrl = defaultUrl;
			return Redirect(returnUrl);
		}

		#endregion
	}
}
